Skip to content

Walk PATH (and honor defaults.binaries) for agent binary discovery#485

Merged
dgershman merged 2 commits into
mainfrom
feature/crow-484-codex-cursor-path-discovery
Jun 11, 2026
Merged

Walk PATH (and honor defaults.binaries) for agent binary discovery#485
dgershman merged 2 commits into
mainfrom
feature/crow-484-codex-cursor-path-discovery

Conversation

@dgershman

Copy link
Copy Markdown
Collaborator

Closes #484

Summary

  • OpenAICodexAgent.findBinary() and CursorAgent.findBinary() only checked three hardcoded install paths and returned nil for npm-global / nvm / Volta / pnpm / asdf installs, silently hiding installed agents from the per-session picker.
  • Refactors discovery into a default CodingAgent.findBinary() impl: explicit defaults.binaries.<kind> override → PATH walk (ShellEnvironment.findExecutable, same way command -v resolves) → hardcoded fallbackCandidates.
  • Each agent now just declares its fallbackCandidates; the discovery logic lives in one place.
  • AppDelegate.launchMainApp loads config first, populates BinaryOverrides.shared from defaults.binaries, then registers agents and logs the resolved path so users can verify which install was picked up.

Acceptance

  • A system with codex installed via any of these methods registers and shows in the picker:
    • brew install-style at /opt/homebrew/bin/codex (existing path, still covered via fallback)
    • npm i -g @openai/codex under standard npm prefix (resolved via PATH)
    • npm-global under nvm ~/.nvm/versions/node/*/bin/codex (resolved via PATH)
    • Volta, pnpm, bun, asdf/mise (resolved via PATH)
    • ~/.local/bin/codex (existing path, still covered via fallback)
  • Same for cursor (agent binary).
  • Setting defaults.binaries.codex to an explicit path causes Crow to use it regardless of discovery (verified by findBinaryHonorsBinaryOverride test).
  • A system with codex not installed continues to silently skip registration (no regression — findBinary() returns nil when override + PATH + fallback all miss).
  • NSLog now prints "OpenAI Codex agent registered at <resolved path>" so users can verify which install was picked up.
  • Manual workaround documented in the ticket (symlink to ~/.local/bin/codex) stops being necessary.

Note: the Corveil binary picker referenced in #482 has not landed on this branch yet. This PR introduces the defaults.binaries schema field; when #482 merges it just adds a corveil row to the same map.

Test plan

  • swift test --arch arm64 in each of the package roots (CrowCore, CrowCodex, CrowCursor, CrowClaude, CrowPersistence) and at the repo root — all green.
  • New tests: BinaryOverrides (4 cases), ShellEnvironment.findExecutable (3 cases), ConfigDefaults.binaries decode (2 cases), OpenAICodexAgent.findBinary honors/ignores stale override (2 cases).
  • Manual smoke on a machine with codex under nvm only — verify the new "registered at <path>" log line and picker visibility.

🤖 Generated with Claude Code

…W-484)

`OpenAICodexAgent.findBinary()` and `CursorAgent.findBinary()` only checked
three hardcoded paths (`/opt/homebrew/bin/...`, `/usr/local/bin/...`,
`~/.local/bin/...`). Codex is distributed as `npm i -g @openai/codex` — under
nvm/Volta/pnpm/asdf the binary never lands in any of those. When discovery
returned nil, `AppDelegate.launchMainApp` skipped registration, so the
picker silently never showed the agent even though it was installed.

This refactors discovery into a default `CodingAgent.findBinary()` impl:
explicit `defaults.binaries.<kind>` override → PATH walk via
`ShellEnvironment.findExecutable` (same way `command -v` resolves) →
hardcoded `fallbackCandidates`. Per-agent impls are gone; each agent just
declares its fallback list. AppDelegate now loads config before
registration, populates `BinaryOverrides.shared`, and logs the resolved
path so users can verify which install Crow picked up.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: CEB4D4CB-61A8-468F-8A3A-0E72FF5610C6
@dgershman dgershman requested a review from dhilgaertner as a code owner June 11, 2026 17:51

@dhilgaertner dhilgaertner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review

Solid, well-scoped refactor: collapsing three near-identical findBinary() impls into one default on CodingAgent (override → PATH walk → fallback) is the right shape, the kind raw-values line up with the JSON keys, the stale-override fall-through is handled, and the config field decodes forward-compatibly. Verified launchCommandToken (codex/agent/claude) and kind (codex/cursor/claude-code) both resolve correctly, and CrowCore builds clean. One blocking test defect and one behavioral concern.

Critical Issues

🔴 Red — BinaryOverrides test suite is racy and fails reproducibly under parallel execution
Packages/CrowCore/Tests/CrowCoreTests/BinaryOverridesTests.swift

All four tests mutate the process-global BinaryOverrides.shared singleton, and Swift Testing runs tests in parallel within a suite by default. The per-test defer { set([:]) } resets don't isolate anything — a concurrent test's set(...) clobbers another's state mid-assertion. The PR's "all green" claim doesn't hold: running swift test --filter BinaryOverrides 6× here failed 5 times, e.g.:

✘ emptyByDefault() — (path(for: .cursor) → "/tmp/agent") == nil
✘ setStoresRawKeyedByAgentKind() — (path(for: .cursor) → nil) == "/tmp/agent"
✘ setReplacesPreviousMap() — (path(for: .cursor) → nil) == "/tmp/agent"

This will flake CI. Fix: serialize the suite — @Suite("BinaryOverrides", .serialized) — and likewise add .serialized to @Suite("OpenAICodexAgent") in Packages/CrowCodex/Tests/CrowCodexTests/OpenAICodexAgentTests.swift, since its two new findBinary* tests mutate the same global (that suite passed 6/6 here only by timing luck — findBinaryHonorsBinaryOverride vs findBinaryIgnoresOverrideWhenPathMissing race on codex). .serialized is sufficient because only these two suites touch the global, and they live in separate targets.

Code Quality

🟡 Yellow — PATH walk for Cursor's generic agent binary name risks false-positive registration
Packages/CrowCore/Sources/CrowCore/Agent/CodingAgent.swift:129 + CursorAgent.launchCommandToken = "agent"

The old impl checked only three explicit .../agent paths. The new PATH walk registers Cursor for any executable named agent anywhere on the user's $PATH. agent is a notably generic name — CI build agents (Azure DevOps, TeamCity), etc., ship a binary literally called agent. On such a machine Crow would now mis-register Cursor and later launch the wrong binary. codex/claude are specific enough not to collide; agent is the outlier. Worth at minimum a code comment acknowledging the tradeoff, and ideally a sanity check (or documenting defaults.binaries.cursor as the escape hatch in the user-facing config docs).

Security Review

Strengths:

  • No injection surface added — findExecutable does filesystem isExecutableFile checks, not shell evaluation, and POSIX empty-PATH entries (which would mean CWD) are dropped by split(separator:), avoiding accidental CWD execution.
  • BinaryOverrides is an opt-in absolute-path override from the user's own config; stale/missing paths fall through safely rather than breaking registration.

Concerns: None beyond the Yellow above.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Request Changes — driven by [1 Red, 1 Yellow, 0 Green] findings.


🐦‍⬛ Reviewed by Crow via Claude Code

…lision risk

Reviewer (dhilgaertner) caught two issues on #485:

1. (Red) `BinaryOverrides` and `OpenAICodexAgent` suites mutate the
   process-global `BinaryOverrides.shared` singleton. Swift Testing runs tests
   in parallel within a suite by default, so the per-test `defer { set([:]) }`
   resets don't isolate anything — a concurrent `set(...)` clobbers another
   test's state mid-assertion (the reviewer reproduced 5/6 failures under
   `--filter`). Marking both suites `.serialized` is sufficient because only
   these two touch the global, and they live in separate test targets.

2. (Yellow) Cursor's CLI binary is named `agent`, which is generic enough
   that CI runner installs (Azure DevOps, TeamCity) ship a binary with the
   same name. The PATH walk in the default `findBinary()` could mis-register
   Cursor on a build machine. Document the tradeoff at the `launchCommandToken`
   declaration and point users at `defaults.binaries.cursor` as the escape
   hatch — the explicit-override step runs before the PATH walk and pins the
   resolution.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: CEB4D4CB-61A8-468F-8A3A-0E72FF5610C6
@dgershman dgershman requested a review from dhilgaertner June 11, 2026 18:00

@dhilgaertner dhilgaertner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review

Refactors per-agent binary discovery into a single default CodingAgent.findBinary() impl: explicit defaults.binaries.<kind> override → PATH walk (ShellEnvironment.findExecutable) → hardcoded fallbackCandidates. This correctly fixes the CROW-484 gap where npm-global / nvm / Volta / pnpm / asdf installs were invisible. Verified by building and running the full test suites for CrowCore, CrowCodex, CrowCursor, and CrowClaude — all green (269 + 29 + 29 + 41 tests).

Critical Issues

None.

Security Review

Strengths:

  • No new attack surface: discovery is read-only (isExecutableFile), no shell-out, no command construction from user input.
  • Stale-override handling is correct and defensive — findBinary() re-verifies isExecutableFile(atPath:) on the configured path before returning it (CodingAgent.swift:124), so a moved/uninstalled binary falls through to PATH/fallback rather than pinning a dead path or breaking registration. Covered by findBinaryIgnoresOverrideWhenPathMissing.
  • BinaryOverrides singleton is properly guarded by NSLock; test suites that mutate the shared state are .serialized to avoid cross-suite leakage.

Concerns:

  • None blocking. The Cursor agent PATH-collision risk (a CI runner's agent binary could resolve as Cursor's CLI on a build machine) is real but is thoroughly documented at CursorAgent.swift and fully mitigated by the defaults.binaries.cursor escape hatch. Acceptable tradeoff for a desktop workstation app.

Code Quality

  • Clean abstraction: the discovery logic now lives in one place and each agent just declares fallbackCandidates; hasCommand is correctly re-expressed as findExecutable(_:) != nil so the two stay in sync (pinned by hasCommandAgreesWithFindExecutable).
  • ConfigDefaults.binaries decoding is forward/backward-compatible — a missing key decodes to [:] (configDefaultsBinariesDefaultsEmpty), and the field round-trips through the synthesized encoder.
  • AppDelegate.launchMainApp config-load reorder is safe: config is loaded once up front, BinaryOverrides.shared is populated before the registration gates run, and the later duplicate load was correctly removed. The new registered at <path> NSLog aids field debugging.
  • Good test coverage across all three layers (override map, PATH resolver, config decode, agent-level honor/ignore).

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by [0 Red, 0 Yellow, 1 Green] findings. The single Green item (Cursor agent name collision) is already documented and mitigated in the PR.


🐦‍⬛ Reviewed by Crow via Claude Code

@dgershman dgershman merged commit 11c3565 into main Jun 11, 2026
2 checks passed
@dgershman dgershman deleted the feature/crow-484-codex-cursor-path-discovery branch June 11, 2026 18:34
dgershman added a commit that referenced this pull request Jun 11, 2026
…87) (#489)

Closes #487

## Summary

- **Scaffolder**: build a per-devroot symlink farm at
`${devRoot}/.claude/bin/<name>` from every `defaults.binaries` entry
whose target is executable. Loop is idempotent, reaps stale symlinks
when keys are removed, and skips non-executable / nonexistent targets so
a misconfigured path never shadows a working PATH install.
- **TmuxBackend**: `configure(crowBinDir:)` threads the bin dir through;
`registerTerminal` exports `CROW_BIN_DIR` and seeds the spawned window's
`PATH` with the bin dir in front via `tmux new-window -e`.
- **Shell wrapper**: re-prepends `$CROW_BIN_DIR` to `PATH` after
sourcing the user's `.zshrc` / `.bashrc` so a user `export PATH=…` can't
shadow the symlink farm. Fish / unknown-shell branches inherit the
already-seeded `PATH` from the wrapper's outer scope.

## Effect

```
agent shell> which corveil
/Users/you/devroot/.claude/bin/corveil
agent shell> corveil --version          # the binary you configured in Settings → General
```

Embedded skills like `/query-corveil` (shipped inside the `corveil`
binary itself) keep using bare `corveil` invocations — Crow makes those
resolve to the user-configured binary without touching the skill
content. Generic in the map: `codex`, `cursor`, and any future tools
share the same mechanism via #484's existing `defaults.binaries` schema.

## Test plan

- [x] `swift test --filter "ScaffolderBinarySymlinkTests"` — 6 tests
covering executable targets, non-executable skip, nonexistent skip,
stale reaping, non-symlink files left alone, reconfiguration re-points.
- [x] `swift test --filter "TmuxBackendCrowBinDirTests"` —
`configure(crowBinDir:)` propagation + default empty.
- [ ] Manual: set `defaults.binaries.corveil` in Settings to an
executable; open a Claude Code session; run `which corveil` → reports
`${devRoot}/.claude/bin/corveil`; `corveil --version` matches the
configured binary; `/query-corveil` invokes that binary.
- [ ] Manual: unset `defaults.binaries.corveil`; relaunch; symlink
removed; bare `corveil` falls back to PATH (or unresolved if not
installed).
- [ ] Manual: set `defaults.binaries.corveil` to a nonexistent path;
relaunch; one-line warning in `[Scaffolder]` log; no broken symlink
created.
- [ ] Manual: also set `defaults.binaries.codex` /
`defaults.binaries.cursor`; both symlinks materialize.

## Related

- #482 — Corveil CLI picker that introduced `defaults.binaries.corveil`.
- #485 — PATH-walk + `defaults.binaries` override for agent binary
discovery; this PR reuses its schema (`[String: String]` map).

🐦‍⬛ Generated with Crow via Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex (and Cursor) binary discovery is hardcoded to 3 paths — misses npm-global / nvm / volta / pnpm installs

2 participants